package org.ddevil.data.set;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.ddevil.data.Data;
import org.ddevil.data.Filter;
import org.ddevil.data.SortOrder;
import org.ddevil.data.StringLexicographicComparator;
import org.ddevil.data.util.DataSetIterator;


/**
 * A SortableDataSet implementation that wraps another DataSet and gives it the
 * ability to apply sorts/filters.
 * <p>
 * This is a {@link org.ddevil.data.set.DataSetWrapper} implementation. If
 * the DataSet being wrapped is changed without this class knowing about it then
 * the results will be undefined.
 *
 * @see org.ddevil.data.set.DataSetWrapper
 * @author Rich O'Connell
 * @author Eric Lundin
 */
public class BasicSortableDataSet<T extends Data> implements SortableDataSet<T>,
		FilterableDataSet<T>, DataSetWrapper {

	private static final long serialVersionUID = 4458598026903386076L;

	/**
	 * Stores the column(s) to be sorted. The first column will be the first to
	 * be sorted and so on. TODO change this to something better
	 */
	protected ArrayList<Sort> sortedColumns;

	static private class Sort {
		String dbIdentifier;

		SortOrder type;

		Sort(String id, SortOrder t) {
			this.dbIdentifier = id;
			this.type = t;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof Sort) {
				Sort o = (Sort) obj;
				return dbIdentifier.equals(o.dbIdentifier);
			}
			return super.equals(obj);
		}
	}

	/** Comparator for Objects that implement {@link java.lang.Comparable} */
	protected static final Comparator OBJECT_COMAPRATOR = new Comparator<Comparable>() {
		@SuppressWarnings("unchecked")
		public int compare(Comparable object1, Comparable object2) {
			return object1.compareTo(object2);
		}
	};

	/** Comparator to use for toString objects. */
	protected static final Comparator TO_STRING_COMPARATOR = new StringLexicographicComparator();

	/** Holds the mapping from view indicies to model indicies. */
	protected ViewRow viewToModel[];

	/** Holds the criteria to be used for filtering. */
	protected HashMap<String, Filter<Object>> filterCriteria;
	
	/** If true all filters will be reversed.
	 *  TODO this should be reworked to be able to apply to individual filters. */
	protected boolean negateFilters = false;

	/** Holds the type of comparator to use for each class type. */
	protected HashMap<Class, Comparator> classComparators;

	/** Holds the type of comparator to use for each column. */
	protected HashMap<String, Comparator> attributeComparators;

	/** The DataSet backing this sorted DataSet */
	protected DataSet<T> model;

	private boolean isSorting = true;

	public BasicSortableDataSet(DataSet<T> model) {
		this.model = model;
		filterCriteria = new HashMap<String, Filter<Object>>();
		classComparators = new HashMap<Class, Comparator>();
		attributeComparators = new HashMap<String, Comparator>();
		resetSort();
		sortedColumns = new ArrayList<Sort>();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#exportRecord(int)
	 */
	public T exportRecord(int rowNum) {
		return model.exportRecord(convertViewToModel(rowNum));
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#exportRecords(int[])
	 */
	public DataSet<T> exportRecords(int... indices) {
		int[] newIndices = new int[indices.length];
		for (int i = 0; i < newIndices.length; i++) {
			newIndices[i] = convertViewToModel(indices[i]);
		}
		return model.exportRecords(newIndices);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#getDataAt(int, java.lang.String)
	 */
	public Object getDataAt(int rowIndex, String dbIdentifier) {
		return model.getDataAt(convertViewToModel(rowIndex), dbIdentifier);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#importRecord(org.ddevil.data.Data)
	 */
	public void importRecord(T inData) {
		model.importRecord(inData);
		resetSort();

	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#importRecords(org.ddevil.data.DataSet)
	 */
	public void importRecords(DataSet<T> inData) {
		model.importRecords(inData);
		resetSort();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#setDataAt(int, java.lang.String,
	 *      java.lang.Object)
	 */
	public void setDataAt(int rowIndex, String dbIdentifier, Object data) {
		model.setDataAt(convertViewToModel(rowIndex), dbIdentifier, data);
		if (getAttributeSortOrder(dbIdentifier) != SortOrder.NONE) {
			resetSort();
		}
		if (filterCriteria.containsKey(dbIdentifier)) {
			resetFilter();
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#removeRecord(int)
	 */
	public void removeRecord(int index) {
		model.removeRecord(convertViewToModel(index));
		resetSort();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#clear()
	 */
	public void clear() {
		sortedColumns.clear();
		filterCriteria.clear();
		classComparators.clear();
		attributeComparators.clear();
		resetSort();
		model.clear();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#getIdentifiers()
	 */
	public List<String> getIdentifiers() {
		return model.getIdentifiers();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSet#size()
	 */
	public int size() {
		return getViewToModel().length;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#setComparator(java.lang.Class,
	 *      java.util.Comparator)
	 */
	public void setComparator(Class className, Comparator myComparator) {
		classComparators.put(className, myComparator);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#setComparator(java.lang.String,
	 *      java.util.Comparator)
	 */
	public void setComparator(String dbId, Comparator myComparator) {
		attributeComparators.put(dbId, myComparator);
	}

	/**
	 * Get the Class that for the given attribute id.
	 *
	 * @param dbId
	 *            The ID of the attribute to get the class for.
	 * @return The Class of the attribute.
	 */
	protected Class< ? > getAttributeClass(String dbId) {
		Object val = model.getDataAt(0, dbId);
		if (val != null) {
			return val.getClass();
		}
		return Object.class;
	}

	/**
	 * Get the comparator to use for the given column index.
	 *
	 * @param columnIndex
	 *            The index of the column to retrieve the comparator for.
	 * @return The comparator to use for the given column
	 */
	protected Comparator getComparator(String dbId) {
		Class< ? > columnClass = getAttributeClass(dbId);
		Comparator myComparator;
		if (attributeComparators.containsKey(dbId))

			myComparator = attributeComparators.get(dbId);

		else if (classComparators.containsKey(columnClass)) {

			myComparator = classComparators.get(columnClass);

		} else if (Comparable.class.isAssignableFrom(columnClass)) {

			myComparator = OBJECT_COMAPRATOR;

		} else {
			myComparator = TO_STRING_COMPARATOR;
		}
		return myComparator;
	}

	/*
	 * (non-Javadoc)
	 */
	public void setSort(String dbId, SortOrder sortOrder) {
		Sort sort = new Sort(dbId, sortOrder);
		if (sortOrder == SortOrder.ASCENDING
				|| sortOrder == SortOrder.DESCENDING) {
			int index = sortedColumns.indexOf(sort);
			if (index > -1) {
				if (sortedColumns.get(index).type != sort.type) {
					sortedColumns.set(index, sort);
					resetSort();
				}
			} else {
				sortedColumns.add(sort);
				resetSort();
			}
		} else {
			// remove the sort if we are storing it
			int index = sortedColumns.indexOf(sort);
			if (index > -1) {
				sortedColumns.remove(index);
				resetSort();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#getSortInfo(java.lang.String)
	 */
	public SortInfo getSortInfo(String dbid) {
		SortInfo ret = null;
		for (int i = 0; i < sortedColumns.size(); i++) {
			Sort s = sortedColumns.get(i);
			if (s.dbIdentifier.equals(dbid)) {
				ret = new SortInfo(s.dbIdentifier, s.type, i);
			}
		}
		return ret;
	}

	/**
	 * probably don't need this specific method
	 *
	 * @param dbid
	 * @return
	 */
	protected SortOrder getAttributeSortOrder(String dbid) {
		for (Sort s : sortedColumns) {
			if (s.dbIdentifier == dbid)
				return s.type;
		}
		return SortOrder.NONE;
	}

	/**
	 * Sort the and return the given ViewRow array.
	 *
	 * @param data
	 * @return
	 */
	protected ViewRow[] sort(ViewRow[] data) {
		Arrays.sort(data);
		return data;
	}

	/**
	 * Resort.
	 */
	public void resetSort() {
		if (isSorting) {
			viewToModel = null;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#clearAllSorts()
	 */
	public void clearAllSorts() {
		sortedColumns.clear();
		resetSort();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#clearAllFilters()
	 */
	public void clearAllFilters() {
		filterCriteria.clear();
		resetFilter();
	}

	/**
	 * Refilter.
	 */
	public void resetFilter() {
		// we have to reset everything incase the filter was made less restrictive
		if (isSorting) {
			viewToModel = null;
		}
	}

	
	public void setNegateFilters(boolean negateFilters) {
		this.negateFilters = negateFilters;
		resetFilter();
	}
	
	/**
	 * Filter the ViewRow array and return it.
	 *
	 * @param data
	 * @return
	 */
	protected ViewRow[] filter(ViewRow[] data) {
		ArrayList<ViewRow> filtered = new ArrayList<ViewRow>();

		for (ViewRow r : data) {
			boolean accept = r.acceptRow();
			if ((!negateFilters && accept) || (negateFilters && !accept)) {
				filtered.add(r);
			}
		}

		ViewRow[] ret = new ViewRow[filtered.size()];
		ret = filtered.toArray(ret);
		return ret;
	}

	public void setFilter(String dbId, Filter<Object> filter) {

		if (filter == null) {
			if (filterCriteria.containsKey(dbId)) {
				filterCriteria.remove(dbId);
				resetFilter();
			}
		} else {
			filterCriteria.put(dbId, filter);
			resetFilter();
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#getFilter(java.lang.String)
	 */
	public Filter<Object> getFilter(String dbid) {
		return filterCriteria.get(dbid);
	}

	/**
	 * Create a mapping from the table's view indicies to the model's data
	 * indicies.
	 *
	 * @return A mapping from the view to the model.
	 */
	protected ViewRow[] getViewToModel() {
		if (viewToModel == null) {

			// populate as is
			ViewRow[] newViewToModel = new ViewRow[model.size()];
			for (int i = 0; i < model.size(); i++) {
				newViewToModel[i] = new ViewRow(i, this);
			}

			// apply sort
			if (!sortedColumns.isEmpty()) {
				newViewToModel = sort(newViewToModel);
			}

			// apply filter
			if (!filterCriteria.isEmpty()) {
				newViewToModel = filter(newViewToModel);
			}

			viewToModel = newViewToModel;
		}
		return viewToModel;
	}

	/**
	 * Convert a view row index to a model row index.
	 *
	 * @param viewIndex
	 * @return
	 */
	protected int convertViewToModel(int viewIndex) {
		return getViewToModel()[viewIndex].getModelIndex();
	}

	/**
	 * This class is for convenience in creating maps for sorting table data.
	 *
	 * @author Eric Lundin
	 *
	 * TODO: this got real messy when generic were introduce. Needed to make this class static
	 * and pass in a ref to the sortable data set owner.  In need of revisitation
	 */
	static class ViewRow implements Comparable<ViewRow> {
		private int modelIndex;
		private BasicSortableDataSet<? extends Data> parent;

		public ViewRow(int modelIndex, BasicSortableDataSet<? extends Data> set) {
			this.modelIndex = modelIndex;
			this.parent = set;
		}

		/**
		 * Decides whether a row should be included in the filtered dataset. If
		 * a given column's filter contains only whitespace that column is
		 * skipped. However it is possible to filter on a combination of
		 * charachters and whitespace. A row is disqualified if one or more of
		 * its columns fails to follow the filter.
		 *
		 * @return Whether or not this row matches the filter.
		 */
		public boolean acceptRow() {
			Filter<Object> currentFilter;
			// String currentCell;
			Object data;
			for (String dbid : parent.filterCriteria.keySet()) {
				data = parent.model.getDataAt(modelIndex, dbid);
				// currentCell = (data != null ? data.toString() : "");
				currentFilter = parent.filterCriteria.get(dbid);
				if (!currentFilter.accept(data)) {
					return false;
				}

			}
			return true;
		}

		/**
		 * Compare this to another object
		 *
		 * @param o
		 *            The other object to comparre this object to.
		 * @return -1 if this object is less than object o, 0 if these objects
		 *         are equal 1 if this object is greater than object o
		 */
		@SuppressWarnings("unchecked")
		public int compareTo(ViewRow o) {
			int row1 = modelIndex;
			int row2 = o.getModelIndex();

			int comparison = 0;
			for (Sort s : parent.sortedColumns) {
				if (s.type == SortOrder.ASCENDING
						|| s.type == SortOrder.DESCENDING) {
					String dbId = s.dbIdentifier;
					Object o1 = parent.model.getDataAt(row1, dbId);
					Object o2 = parent.model.getDataAt(row2, dbId);

					Class cl = parent.getAttributeClass(dbId);

					if (parent.attributeComparators.containsKey(dbId)
							||parent.classComparators.containsKey(cl)) {
						comparison = parent.getComparator(dbId).compare(o1, o2);
					} else {
						// Define null less than everything, except null.
						if (o1 == null && o2 == null) {
							comparison = 0;
						} else if (o1 == null) {
							comparison = -1;
						} else if (o2 == null) {
							comparison = 1;
						} else {
							comparison = parent.getComparator(dbId).compare(o1, o2);
						}
					}
					if (comparison != 0) {
						if (s.type == SortOrder.DESCENDING) {
							comparison = -comparison;
						}
						break;
					}
				}
			}
			return comparison;
		}

		/**
		 * Get the model index of this item.
		 *
		 * @return The model index for this object.
		 */
		public int getModelIndex() {
			return this.modelIndex;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.DataSetWrapper#getWrappedDataSet()
	 */
	public DataSet<T> getWrappedDataSet() {
		return model;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.ddevil.data.SortableDataSet#setSortEnabled(boolean)
	 */
	public void setSortEnabled(boolean sortEnabled) {
		isSorting = sortEnabled;
		resetSort();
		resetFilter();
	}

	public void removeRecords(int... indices) {
		int[] viewIndices = new int[indices.length];
		for (int i = 0; i < indices.length; i++) {
			viewIndices[i] = convertViewToModel(indices[i]);
		}
		Arrays.sort(viewIndices);
		for (int i = viewIndices.length - 1; i >= 0; i--) {
			model.removeRecord(viewIndices[i]);
		}
		resetSort();
		resetFilter();
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterator<T> iterator() {
		return new DataSetIterator<T>(this);
	}

}
